package log

import (
	"context"
	"fmt"
	"log"
	"os"
	"path"
	"path/filepath"
	"strings"
	"sync"
	"time"

	sutil "gitee.com/simplexyz/simpleutil-go"
	sworker "gitee.com/simplexyz/simpleworker-go"
)

type file struct {
	*os.File
	w              *fileWriter
	level          Level
	path           string
	size           int64
	lastRotateTime time.Time
	buffer         []byte
}

func createFile(writer *fileWriter, level Level) (*file, error) {
	f := &file{
		w:              writer,
		level:          level,
		lastRotateTime: time.Now(),
	}
	if f.level.SubName() != "" {
		f.path = filepath.Join(f.w.Dir, f.w.Name+"."+f.level.SubName()+".log")
	} else {
		f.path = filepath.Join(f.w.Dir, f.w.Name+".log")
	}
	var err error
	f.File, err = os.OpenFile(f.path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		return nil, err
	}
	info, err := os.Lstat(f.path)
	if err != nil {
		return nil, err
	}
	f.size = info.Size()
	return f, nil
}

func (f *file) write(record IRecord) bool {
	level := record.Level()

	if f.level != LevelAll && f.level != level {
		return false
	}

	f.buffer = append(f.buffer, record.Bytes()...)

	if level >= LevelError {
		f.sink()
		return true
	}

	if len(f.buffer) >= 1024 {
		f.sink()
		return true
	}

	return false
}

func (f *file) sink() {
	if len(f.buffer) == 0 {
		return
	}
	n, _ := f.File.Write(f.buffer)
	f.size += int64(n)
	f.buffer = nil
}

func (f *file) rotate() {
	_ = f.Sync()
	_ = f.Close()

	oldPath := f.path
	newPath := f.path
	nowTime := time.Now()
	timestamp := nowTime.Format("2006-01-02-15-04-05")
	if nowTime.Sub(f.lastRotateTime) < time.Second {
		ms := nowTime.Format(".000")
		ms = strings.Replace(ms, ".", "-", 1)
		timestamp += ms
	}
	if f.level.SubName() != "" {
		newPath = filepath.Join(f.w.Dir, fmt.Sprintf("%s.%s.%s.log", f.w.Name, f.level.SubName(), timestamp))
	} else {
		newPath = filepath.Join(f.w.Dir, fmt.Sprintf("%s.%s.log", f.w.Name, timestamp))
	}
	_ = os.Rename(oldPath, newPath)

	f.File, _ = os.OpenFile(f.path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	f.size = 0
	f.lastRotateTime = nowTime
}

type fileWriter struct {
	*sworker.Worker

	FileOption

	stoppedWg sync.WaitGroup

	files []*file

	ticker        *time.Ticker
	lastCheckTime time.Time

	queue chan IRecord
}

func CreateFileWriter(option *FileOption) (IWriter, error) {
	if option == nil {
		option = &FileOption{}
	}

	w := &fileWriter{
		FileOption:    *option,
		lastCheckTime: time.Now(),
	}

	var err error

	if w.Name == "" {
		// 默认使用可执行文件名（不带扩展名）
		w.Name, err = sutil.GetProgramFileBaseName()
		if err != nil {
			return nil, fmt.Errorf("init file writer name fail, %w", err)
		}
	}

	if _, ok := GetFileWriter(w.Name); ok {
		return nil, fmt.Errorf("file writer already exist")
	}

	if w.Dir == "" {
		// 默认保存在当前目录
		w.Dir = "."
	}
	w.Dir = filepath.Join(w.Dir, w.Name)

	if w.MaxSize <= 0 {
		// 默认单个文件最大30MB
		w.MaxSize = 30 * 1024 * 1024
	}

	if w.MaxAge <= 0 {
		// 默认保存30天
		w.MaxAge = 30 * 24 * time.Hour
	}

	if w.SeperatedLevels <= 0 {
		// 默认分离error日志
		w.SeperatedLevels = LevelError
	}

	if w.QueueSize <= 0 {
		w.QueueSize = 1024
	}

	if w.TickInterval <= 0 {
		// 默认间隔3秒
		w.TickInterval = 3 * time.Second
	}

	w.ticker = time.NewTicker(w.TickInterval)

	err = sutil.MkDir(w.Dir)
	if err != nil {
		return nil, fmt.Errorf("create directory fail, %w", err)
	}

	f, e := createFile(w, LevelAll)
	if e != nil {
		return nil, fmt.Errorf("create file fail, path=[%s], %w", f.path, e)
	}

	w.files = append(w.files, f)

	if w.SeperatedLevels > 0 {
		EachLevel(func(level Level) (continued bool) {
			if !level.IsIn(w.SeperatedLevels) {
				return true
			}
			f, e = createFile(w, level)
			if e != nil {
				err = fmt.Errorf("open f fail, path=[%s], %w", f.path, e)
				return false
			}
			w.files = append(w.files, f)
			return true
		})
		if err != nil {
			return nil, err
		}
	}

	w.queue = make(chan IRecord, w.QueueSize)

	w.Worker, err = sworker.Create(
		nil, true,
		sworker.WithOnWorking(w.onWorking),
		sworker.WithOnStop(w.onStop),
		sworker.WithStopWaiter(&w.stoppedWg),
	)
	if err != nil {
		writers.Store(w.Name, w)
	}

	return w, err
}

func MustCreateFileWriter(option *FileOption) IWriter {
	w, err := CreateFileWriter(option)
	if err != nil {
		panic(err)
	}
	return w
}

func (w *fileWriter) Write(record IRecord) {
	if !w.Working() {
		return
	}
	if record == nil {
		return
	}
	w.queue <- record
}

func (w *fileWriter) write(record IRecord) {
	for _, f := range w.files {
		if f.write(record) {
			w.tryRotate(f)
		}
	}

	destroyRecord(record)
}

func (w *fileWriter) Destroy() {
	w.Worker.Stop(func() {
		close(w.queue)
	}, true)
}

func (w *fileWriter) onWorking(ctx context.Context) bool {
	for {
		select {
		case <-ctx.Done():
			for r := range w.queue {
				w.write(r)
			}
			return false

		case <-w.ticker.C:
			w.onTimeout()

		case r, ok := <-w.queue:
			if ok {
				w.write(r)
			}
		}
	}
}

func (w *fileWriter) onStop() {
	w.ticker.Stop()
}

func (w *fileWriter) tryRotate(f *file) bool {
	if f.size >= w.MaxSize {
		f.rotate()
		return true
	}
	if !sutil.IsSameDay(time.Now(), w.lastCheckTime) {
		f.rotate()
		return true
	}
	return false
}

func (w *fileWriter) onTimeout() {
	//log.Println("onTimeout")

	// 滚转
	for _, f := range w.files {
		f.sink()
		w.tryRotate(f)
	}

	now := time.Now()

	var needBackupFilePaths []string

	// 将超时文件归档，然后删除
	//log.Print("remove timeout file begin")
	_ = sutil.Walk(w.Dir, "log", nil, nil, func(path string, info os.FileInfo, name string, isInBlackList bool) (continued bool) {
		//log.Printf("path=%s", path)

		for _, f := range w.files {
			if info.Name() == sutil.GetFileName(f.path) {
				//log.Printf("ignore file %s", path)
				return true
			}
		}

		if now.Sub(info.ModTime()) < w.MaxAge {
			return true
		}

		needBackupFilePaths = append(needBackupFilePaths, path)
		log.Printf("find time out file %s", path)
		return true
	})

	if len(needBackupFilePaths) > 0 {
		timestamp := now.Format("2006-01-02-15-04-05")
		// 单独起一个协程执行来避免阻塞
		//w.stoppedWg.Add(1)
		//go func() {
		backupFilePath := path.Join(w.Dir, fmt.Sprintf("%s.%s.log.zip", w.Name, timestamp))
		backupFilePath, _ = filepath.Abs(backupFilePath)
		if err := sutil.Zip(needBackupFilePaths, backupFilePath); err != nil {
			log.Printf("backup log file to %s fail, %v", backupFilePath, err)
		} else {
			log.Printf("backup log file to %s success", backupFilePath)
		}
		for _, p := range needBackupFilePaths {
			_ = os.Remove(p)
		}
		//w.stoppedWg.Done()
		//}()
	}
	//log.Print("remove timeout file end")

	w.lastCheckTime = now
}
